'''
Date: 2025-07-15 17:25:46
Description: OpenCV机器视觉识别
FilePath: \openCV\openCV.py
'''
import sys
import io
import json
import pyautogui
import numpy as np
import os
import time
# 方法一：使用 decimal 模块（精确控制舍入规则）
from decimal import Decimal, ROUND_HALF_UP

import filter_candidate
import get_message
import get_message_detail
import re, uiautomation as auto
import input_message
import choose_post
import sync_post
import sex_model
import get_message_person_info
import click_cancel_filter
import get_message_choose_post

from change_window import fit_window_left_top_uia

# 设置utf-8编码
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# 关键：强制标准流使用 UTF-8
if hasattr(sys.stdin, "reconfigure"):
    sys.stdin.reconfigure(encoding="utf-8")
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")
else:
    # 兼容非常老的版本
    sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')


# 解决中文路径读取图像问题
def imread_unicode(path):
    try:
        return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)
    except:
        return None


state = {"ocr": None}


def non_max_suppression(boxes, overlapThresh):
    if len(boxes) == 0:
        return []
    boxes = np.array(boxes)
    pick = []

    x1 = boxes[:, 0]  # 左边界
    y1 = boxes[:, 1]  # 上边界
    x2 = boxes[:, 2]  # 右边界
    y2 = boxes[:, 3]  # 下边界
    scores = boxes[:, 4]  # 置信度（OpenCV匹配结果）

    area = (x2 - x1 + 1) * (y2 - y1 + 1)
    idxs = np.argsort(scores)

    while len(idxs) > 0:
        last = len(idxs) - 1
        i = idxs[last]
        pick.append(i)

        # 计算重叠区域
        xx1 = np.maximum(x1[i], x1[idxs[:last]])
        yy1 = np.maximum(y1[i], y1[idxs[:last]])
        xx2 = np.minimum(x2[i], x2[idxs[:last]])
        yy2 = np.minimum(y2[i], y2[idxs[:last]])

        w = np.maximum(0, xx2 - xx1 + 1)
        h = np.maximum(0, yy2 - yy1 + 1)

        overlap = (w * h) / area[idxs[:last]]

        # 删除重叠度超过阈值的框
        idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0])))

    return boxes[pick]


count = 1


def increment():
    global count  # 声明要使用全局变量 count
    count += 1  # 修改全局变量的值


from PIL import Image
import uuid


def get_sex(left_region, width_ratio=0.2, height_ratio=0.45):
    """
    按比例截取图像左上角区域并保存

    参数:
        left_region: OpenCV格式图像(numpy.ndarray)
        width_ratio: 截取宽度占原图宽度的比例(0-1之间)
        height_ratio: 截取高度占原图高度的比例(0-1之间)
    """
    # 获取原始图像尺寸
    height, width = left_region.shape[:2]  # OpenCV图像格式为(高, 宽, 通道)

    # 根据比例计算截取区域的实际尺寸
    crop_width = int(width * width_ratio)
    crop_height = int(height * height_ratio)

    # 确保截取尺寸至少为1像素，避免无效区域
    crop_width = max(1, crop_width)
    crop_height = max(1, crop_height)

    # 截取左上角区域（OpenCV使用数组切片）
    # 格式：[行范围, 列范围] -> [y1:y2, x1:x2]
    cropped_region = left_region[0:crop_height, 0:crop_width]

    # 转换为PIL格式并保存
    pil_image = Image.fromarray(cv2.cvtColor(cropped_region, cv2.COLOR_BGR2RGB))
    # save_path = f'截取头像_{uuid.uuid4()}.png'
    # pil_image.save(save_path)
    # print(f"已保存按比例截取的头像: {save_path}", file=sys.stderr)

    predict_result = sex_model.sex_main('assets/icon_gender_model.pth', pil_image)
    # print("预测性别结果", predict_result, file=sys.stderr)

    sex_result = predict_result['prediction']
    if sex_result == 'male_icon':
        return '男'
    elif sex_result == 'female_icon':
        return '女'

    return '未知'

    # # 直接在代码中设置参数
    # INPUT_PATH = "left_region_2.png"  # 替换为你的输入图片路径
    # OUTPUT_PATH = "cropped_avatar.png"  # 替换为你的输出图片路径
    #
    # # 裁剪区域参数（比例值，范围0.0-1.0）
    # LEFT_RATIO = 0.09  # 从图片宽度的10%位置开始（向右偏移）
    # UPPER_RATIO = 0.05  # 从图片高度的10%位置开始（向下偏移）
    # RIGHT_RATIO = 0.22  # 裁剪到图片宽度的60%位置
    # LOWER_RATIO = 0.43  # 裁剪到图片高度的60%位置
    #
    # # 执行裁剪
    # cropped_img = crop_image_by_ratio(left_region, OUTPUT_PATH,
    #                                   LEFT_RATIO, UPPER_RATIO,
    #                                   RIGHT_RATIO, LOWER_RATIO)
    #
    # predict_result = sex_main('assets/icon_gender_model.pth', cropped_img)
    # print("预测性别结果", predict_result,file=sys.stderr)
    #
    # sex_result = predict_result['prediction']
    # if sex_result == 'male_icon':
    #     return '男'
    # elif sex_result == 'female_icon':
    #     return '女'
    #
    #
    # # 更新计数
    # increment()
    # return ''


def crop_image_by_ratio(left_region, output_path,
                        left_ratio=0.0, upper_ratio=0.0,
                        right_ratio=0.5, lower_ratio=0.5):
    """
    按相对比例裁剪图片的指定区域

    参数:
    input_path (str): 输入图片路径
    output_path (str): 输出图片路径
    left_ratio (float): 裁剪区域左边界占原图宽度的比例，范围0.0-1.0
    upper_ratio (float): 裁剪区域上边界占原图高度的比例，范围0.0-1.0
    right_ratio (float): 裁剪区域右边界占原图宽度的比例，范围0.0-1.0
    lower_ratio (float): 裁剪区域下边界占原图高度的比例，范围0.0-1.0
    """

    # 将left_region转换为PIL.Image对象
    left_region = Image.fromarray(cv2.cvtColor(left_region, cv2.COLOR_BGR2RGB))

    # 获取原始尺寸
    original_width, original_height = left_region.size

    # 计算裁剪区域的像素坐标
    left = int(original_width * left_ratio)
    upper = int(original_height * upper_ratio)
    right = int(original_width * right_ratio)
    lower = int(original_height * lower_ratio)

    # 确保右边界大于左边界，下边界大于上边界
    if right <= left or lower <= upper:
        raise ValueError("右边界和下边界必须分别大于左边界和上边界")

    # 定义裁剪框
    crop_box = (left, upper, right, lower)

    # 裁剪图片
    cropped_img = left_region.crop(crop_box)

    return cropped_img

    # 保存裁剪后的图片
    # cropped_img.save(output_path)
    # print(f"成功裁剪图片: {input_path} -> {output_path}", file=sys.stderr)
    # print(f"裁剪区域: 左={left}, 上={upper}, 右={right}, 下={lower}", file=sys.stderr)
    # print(f"裁剪尺寸: {cropped_img.width}x{cropped_img.height} 像素", file=sys.stderr)


import pyautogui
import numpy as np
import cv2


# 获取个人信息start======================================================================================


def preprocess_image(img):
    """图像预处理以提高OCR识别率，并确保输出为三通道格式"""
    # 1. 转换为灰度图（单通道）
    # gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # # 2. 自适应阈值二值化
    # thresh = cv2.adaptiveThreshold(
    #     gray, 255,
    #     cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    #     cv2.THRESH_BINARY, 11, 2
    # )
    # # 3. 降噪处理
    # kernel = np.ones((1, 1), np.uint8)
    # processed_img = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    # # 4. 关键修复：将单通道图像转回三通道（BGR格式），满足OCR模型要求
    # processed_img = cv2.cvtColor(processed_img, cv2.COLOR_GRAY2BGR)
    # return processed_img

    processed_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
    return processed_img


def round_half_up(num):
    return float(Decimal(str(num)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))




# 获取个人信息ends======================================================================================


# 匹配单个图片，返回一个坐标
def get_matches_one_with_confidence(template_path, region=None,scale_range=(0.5, 2.0), num_scales=20, threshold=0.9):
    """在不同尺度下执行模板匹配，返回最佳匹配的位置、尺寸、缩放比例和相似度"""

    # 截取屏幕
    if region:
        # 如果指定了区域，只截取该区域
        screenshot = pyautogui.screenshot(region=region)  # region=(left, top, width, height)
        # 保存指定区域的截图
        # screenshot.save(f"region_{region[0]}_{region[1]}_{region[2]}_{region[3]}.png")
    else:
        # 否则截取全屏
        screenshot = pyautogui.screenshot()

    # 转换为灰度图
    screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
    gray_image = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
    template = imread_unicode(template_path)
    gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)

    # 模板尺寸
    h, w = gray_template.shape

    best_match = None
    best_scale = 1.0
    best_score = 0.0  # 最佳匹配得分

    # 在不同尺度下尝试匹配
    for scale in np.linspace(scale_range[0], scale_range[1], num_scales):
        # 调整模板大小
        resized_template = cv2.resize(gray_template, (int(w * scale), int(h * scale)))

        # 如果调整后的模板比原图大，则停止
        if resized_template.shape[0] > gray_image.shape[0] or resized_template.shape[1] > gray_image.shape[1]:
            break

        # 执行模板匹配
        result = cv2.matchTemplate(gray_image, resized_template, cv2.TM_CCOEFF_NORMED)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

        # 记录最佳匹配（包括得分）
        if best_match is None or max_val > best_score:
            best_score = max_val  # 更新最佳得分
            best_match = (max_val, max_loc, min_loc)
            best_scale = scale

    # 检查是否达到相似度阈值
    if best_score < threshold:
        # 未找到符合要求的匹配
        print(f"相似度: {best_score:.4f}, 阈值: {threshold}", file=sys.stderr)
        return []  # 未达到阈值，返回None表示匹配失败

    # 计算最佳匹配位置和尺寸
    _, location, _ = best_match
    h, w = gray_template.shape
    h, w = int(h * best_scale), int(w * best_scale)

    results = []
    if location is None:
        # 未找到符合要求的匹配
        print(f"相似度: {best_score:.4f}, 阈值: {threshold}", file=sys.stderr)
    else:
        # 计算匹配到的图片的中心坐标
        center_x = location[0] + w // 2
        center_y = location[1] + h // 2
        center_coordinate = (center_x, center_y)

        # print(f"匹配成功! 缩放比例: {scale:.2f}", file=sys.stderr)
        # print(f"匹配相似度: {best_score:.4f}（阈值: {threshold}）", file=sys.stderr)
        # print(f"匹配到的图片的中心坐标: {center_coordinate}", file=sys.stderr)

        results.append({
            "center": center_coordinate,
            "confidence": best_score,
            "scale": scale,
        })

    return results


# 匹配多个图片，返回多个坐标
def get_matches_with_confidence(image_path, region=None, scale_range=(0.5, 2.0), num_scales=20, confidence=0.9,
                                min_dist=20):
    """返回所有非重复的匹配点"""

    # 截取屏幕
    if region:
        # 如果指定了区域，只截取该区域
        screenshot = pyautogui.screenshot(region=region)  # region=(left, top, width, height)
        # 保存指定区域的截图
        # screenshot.save(f"region_{region[0]}_{region[1]}_{region[2]}_{region[3]}.png")
    else:
        # 否则截取全屏
        screenshot = pyautogui.screenshot()
        # 保存全屏截图
        # screenshot.save(f"full_{time.time()}.png")
    screenshot_cv = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    # 读取模板
    template = imread_unicode(image_path)

    gray_image = cv2.cvtColor(screenshot_cv, cv2.COLOR_BGR2GRAY)
    gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)

    h, w = gray_template.shape
    matches = []

    for scale in np.linspace(scale_range[0], scale_range[1], num_scales):
        resized_template = cv2.resize(gray_template, (int(w * scale), int(h * scale)))
        if resized_template.shape[0] > gray_image.shape[0] or resized_template.shape[1] > gray_image.shape[1]:
            continue

        result = cv2.matchTemplate(gray_image, resized_template, cv2.TM_CCOEFF_NORMED)
        loc = np.where(result >= confidence)

        for pt in zip(*loc[::-1]):  # pt 是左上角坐标
            match_w, match_h = resized_template.shape[1], resized_template.shape[0]
            center_x = int(pt[0] + match_w // 2)
            center_y = int(pt[1] + match_h // 2)
            center = (center_x, center_y)
            score = float(result[pt[1], pt[0]])  # 获取匹配相似度值

            # 检查是否距离太近，避免重复
            is_duplicate = False
            for m in matches:
                dist = np.linalg.norm(np.array(center) - np.array(m['center']))
                if dist < min_dist:
                    is_duplicate = True
                    break
            if not is_duplicate:
                matches.append({
                    'top_left': pt,
                    'center': center,
                    'scale': scale,
                    'size': (match_w, match_h),
                    'score': score  # 新增
                })

    # 排序：先按 y，再按 x 从小到大
    matches.sort(key=lambda m: (m['center'][1], m['center'][0]))

    results = []

    if matches:
        result = gray_template.copy()
        for idx, match in enumerate(matches):
            top_left = match['top_left']
            w, h = match['size']
            center = match['center']
            scale = match['scale']
            confidence = match['score']

            # cv2.rectangle(result, top_left, (top_left[0] + w, top_left[1] + h), (0, 255, 0), 2)
            # cv2.circle(result, center, 5, (0, 255, 255), -1)
            # cv2.putText(result, f'{idx + 1}: {scale:.2f}', (top_left[0], top_left[1] - 10), cv2.FONT_HERSHEY_SIMPLEX,
            #             0.6, (255, 0, 0), 1)
            print(f"[{idx + 1}] 匹配到的位置：{center}，置信值：{confidence}，缩放：{scale:.2f}", file=sys.stderr)
            results.append({
                "center": center,
                "confidence": confidence,
                "scale": scale,
            })
        return results

        # 显示所有匹配结果（可选）
        # cv2.imshow('所有匹配结果', result)
        # cv2.waitKey(0)
        # cv2.destroyAllWindows()
    else:
        print("没有匹配到模板。", file=sys.stderr)

    return results






# -*- coding: utf-8 -*-
"""
只处理“整行完全可见”的项 + 性能优化版
依赖：pywinauto, mss, pillow
pip install pywinauto mss pillow
"""
import sys
import time
import os
import ctypes
import mss, mss.tools
from pywinauto.application import Application
from pywinauto import timings, Desktop
from pywinauto.controls.uia_controls import ButtonWrapper
from PIL import Image

# --- DPI 感知，防止高分屏坐标错位 ---
try:
    ctypes.windll.user32.SetProcessDPIAware()
except Exception:
    pass

# === 按需修改 ===
TARGET_PID = None               # <<< 用诊断脚本得到的 pid
BUTTON_TITLE = "打招呼"
AVATAR_DIR = "avatars"
AVATAR_ICON_DIR = "avatars_icons"
os.makedirs(AVATAR_DIR, exist_ok=True)
os.makedirs(AVATAR_ICON_DIR, exist_ok=True)

# pywinauto 时间参数（适当调小）
timings.Timings.window_find_timeout = 6
timings.Timings.after_clickinput_wait = 0.01

# 全局超时（秒），防止长时间卡住
GLOBAL_TIMEOUT = 30


# ---------------- 工具函数 ----------------
def fully_visible(rect, view_rect, pad=12):
    """rect 是否完全可见于 view_rect（四周留 pad 安全边距）"""
    return (
        rect.top    >= view_rect.top + pad and
        rect.bottom <= view_rect.bottom - pad and
        rect.left   >= view_rect.left + pad and
        rect.right  <= view_rect.right - pad
    )

def is_row_container(ctrl):
    """简单规则：行容器高度约 100~300，子控件>=3"""
    try:
        r = ctrl.rectangle(); h = r.height()
        return 100 <= h <= 300 and len(ctrl.children()) >= 3
    except Exception:
        return False

def climb_to_row(btn, max_steps=5):
    """从按钮向上找行容器；最多爬 max_steps 层"""
    cur = btn.parent(); steps = 0
    while cur and steps < max_steps:
        if is_row_container(cur): return cur
        cur = cur.parent(); steps += 1
    return btn.parent()

def iter_kids_limited(root, max_depth=2):
    """有限深度遍历（极大降低开销）：root 的子孙，深度<=max_depth"""
    stack = [(root, 0)]
    while stack:
        node, d = stack.pop()
        if d > max_depth:
            continue
        # 只在需要时才取 children（避免无谓展开）
        try:
            kids = node.children()
        except Exception:
            kids = []
        for k in kids:
            yield k, d+1
            if d+1 < max_depth:
                stack.append((k, d+1))

def get_left_texts_in_row_fast(row, btn_rect):
    """有限深度遍历 + 仅按钮左侧区域"""
    left_edge = row.rectangle().left
    right_edge = btn_rect.left - 8
    items = []
    for t, _ in iter_kids_limited(row, max_depth=2):
        try:
            if t.element_info.control_type != "Text":
                continue
            rect = t.rectangle()
            if rect.left >= left_edge and rect.right <= right_edge:
                s = (t.window_text() or "").strip()
                if s:
                    items.append((rect.top, s))
        except Exception:
            pass
    items.sort(key=lambda x: x[0])
    return [s for _, s in items]

def normalize_lines(lines):
    out, seen = [], set()
    for s in (l.strip() for l in lines):
        if not s or s in seen:
            continue
        seen.add(s)
        out.append(s)
    return out

def find_avatar_rect_fast(row, btn_rect):
    """有限深度遍历找头像（只看左侧区域）"""
    row_rect = row.rectangle()
    left_edge = row_rect.left
    right_edge = btn_rect.left - 8
    allow_types = {"Image", "Button", "Hyperlink", "Custom", "Pane"}
    cands = []
    btn_cy = (btn_rect.top + btn_rect.bottom) / 2.0

    for c, _ in iter_kids_limited(row, max_depth=2):
        try:
            info = c.element_info
            ctype = getattr(info, "control_type", "") or ""
            if ctype not in allow_types:
                continue
            r = c.rectangle()
            if r.left < left_edge or r.right > right_edge:
                continue
            w, h = r.width(), r.height()
            if w < 24 or h < 24 or w > 200 or h > 200:
                continue
            ar = w / float(h)
            if not (0.6 <= ar <= 1.4):
                continue
            score = 1000.0 / (1.0 + (r.left - left_edge))
            score += 300.0 / (1.0 + abs(ar - 1.0) * 10.0)
            score += 200.0 / (1.0 + abs(((r.top + r.bottom) / 2.0) - btn_cy))
            score += min(w, h) * 0.5
            cands.append((score, (int(r.left), int(r.top), int(r.right), int(r.bottom))))
        except Exception:
            pass

    if not cands:
        return None
    return sorted(cands, key=lambda x: x[0], reverse=True)[0][1]

def crop_screenshot_to_file(bbox, out_path):
    L, T, R, B = map(int, bbox)
    W, H = max(1, R - L), max(1, B - T)
    with mss.mss() as sct:
        grab = sct.grab({"left": L, "top": T, "width": W, "height": H})
        mss.tools.to_png(grab.rgb, grab.size, output=out_path)
    return out_path

def guess_view_rect(main_win):
    """优先尝试找到带 ScrollBar 的容器作为可视区域；失败就用主窗体矩形"""
    try:
        # 只做浅层搜索，避免过慢
        for pane in main_win.children():
            try:
                # 有时列表容器是 Pane/Group/Custom 之一
                if pane.element_info.control_type in ("Pane", "Group", "Custom"):
                    # 浅层找滚动条
                    has_sb = any(x.element_info.control_type == "ScrollBar" for x in pane.children())
                    if has_sb:
                        return pane.rectangle()
            except Exception:
                pass
    except Exception:
        pass
    return main_win.rectangle()








_win_cache = None
_win_filter_candidate_cache = None


def get_main_window():
    global _win_cache, TARGET_PID
    if _win_cache is not None :
        return _win_cache

    print('初始化 进程ID', file=sys.stderr)
    pids = get_pid_by_title('BOSS直聘')
    print('BOSS直聘进程ID:', pids, file=sys.stderr)

    if pids is None or len(pids) == 0:
        print('没有获取到BOSS直聘进程ID,请确认是否启动BOSS直聘', file=sys.stderr)
    else:
        TARGET_PID = pids[0]

    wins = [w for w in Desktop(backend="uia").windows(process=TARGET_PID) if w.is_visible()]
    if not wins:
        raise RuntimeError("找不到可见窗口")

    # return wins
    # 默认取面积最大
    main_win = max(wins, key=lambda w: w.rectangle().width() * w.rectangle().height())
    _win_cache = main_win
    return main_win

def get_win_filter_candidate_window(refresh=False):
    global _win_filter_candidate_cache
    if _win_filter_candidate_cache is not None and  refresh == False :
        return _win_filter_candidate_cache
    _win_filter_candidate_cache = Desktop(backend="uia").window(title="BOSS直聘")
    return _win_filter_candidate_cache


# ---------------- 主流程 ----------------
def get_user_info():
    start = time.time()
    main_win = get_main_window()
    if not main_win:
        print("该进程下未发现可见窗口：请确认以管理员运行，或客户端是否最小化/隐藏。",file=sys.stderr)
        return
    duration = time.time() - start
    print(f'初始化all_wins耗时{duration:.2f}秒',file=sys.stdout)

    # main_win = max(all_wins, key=lambda w: (w.rectangle().width() * w.rectangle().height()))
    main_win.set_focus()

    # 1) 一次性找按钮（只按标题筛选，避免巨大树全面遍历）
    # print("[INFO] 正在查找按钮…",file=sys.stderr)
    buttons = main_win.descendants(control_type="Button", title=BUTTON_TITLE)
    print(f"按钮数: {len(buttons)}",file=sys.stderr)
    if not buttons:
        return

    view_rect = guess_view_rect(main_win)
    print(f"可视区: {view_rect}",file=sys.stderr)

    rows_data = []
    avatar_index = 0
    processed = 0

    for b in buttons:
        # 全局超时保护
        if time.time() - start > GLOBAL_TIMEOUT:
            print("[WARN] 超过全局超时限制，提前结束。",file=sys.stderr)
            break

        if not isinstance(b, ButtonWrapper):
            continue

        brect = b.rectangle()

        # 2) 先用按钮矩形做“是否在可视区内”的快速过滤（极大减少后续工作量）
        if not fully_visible(brect, view_rect, pad=10):
            continue

        # 3) 再爬行找到 row（很快，因为只向上找少数层）
        row = climb_to_row(b)
        row_rect = row.rectangle()

        # 4) 用行矩形再做一次严格可见性判断
        if not fully_visible(row_rect, view_rect, pad=20):
            continue

        # 5) 采集（使用有限深度遍历版本）
        left_texts = get_left_texts_in_row_fast(row, brect)
        btn_center_x = int((brect.left + brect.right) // 2)
        btn_center_y = int((brect.top + brect.bottom) // 2)
        avatar_rect = find_avatar_rect_fast(row, brect)

        avatar_path = ""
        gender = ""
        if avatar_rect:
            avatar_index += 1
            avatar_path = os.path.join(AVATAR_DIR, f"avatar_{avatar_index}.png")
            try:
                crop_screenshot_to_file(avatar_rect, avatar_path)

                # 截取头像性别图标
                image = Image.open(avatar_path).convert('RGB')
                icon = sex_model.extract_icon(image)
                icon.save(f'{AVATAR_ICON_DIR}/icon_{avatar_index}.png')

                gender = sex_model.sex_main('assets/icon_gender_model.pth',icon)
                if gender['prediction'] == 'female_icon':
                    gender = '女'
                elif gender['prediction'] == 'male_icon':
                    gender = '男'
                else:
                    gender = '未知'

            except Exception as e:
                print("截取头像失败:", e,file=sys.stderr)
                avatar_path = ""



        rows_data.append({
            "btn_center_y": btn_center_y,
            "btn_center_x": btn_center_x,
            "btn_rect": (brect.left, brect.top, brect.right, brect.bottom),
            "row_rect": (row_rect.left, row_rect.top, row_rect.right, row_rect.bottom),
            "view_rect": (view_rect.left, view_rect.top, view_rect.right, view_rect.bottom),
            "info_lines": left_texts,
            "avatar_rect": avatar_rect,
            "avatar_path": avatar_path,
            "gender": gender
        })
        processed += 1
        if processed % 5 == 0:
            print(f"[INFO] 已处理 {processed} / {len(buttons)}",file=sys.stderr)

    # 6) 文本清洗；若既无文本也无头像，跳过
    rows_data_clean = []
    skipped_empty = 0
    for item in rows_data:
        lines = normalize_lines(item["info_lines"])
        if not lines and not item["avatar_rect"]:
            skipped_empty += 1
            continue
        item["info_lines"] = lines
        rows_data_clean.append(item)

    rows_data_clean.sort(key=lambda x: x["btn_center_y"])

    results=[]

    for i, item in enumerate(rows_data_clean, 1):
        # print(f"\n=== 第 {i} 条 ===",file=sys.stderr)
        # print(f"按钮中心坐标: ({item['btn_center_x']}, {item['btn_center_y']})",file=sys.stderr)
        # print(f"按钮矩形: {item['btn_rect']}",file=sys.stderr)
        # print(f"行矩形: {item['row_rect']}  可视区: {item['view_rect']}",file=sys.stderr)
        # print(f"性别: {item['gender'] or '未知'}",file=sys.stderr)
        if item["avatar_rect"]:
            # print(f"头像矩形: {item['avatar_rect']}",file=sys.stderr)
            # print(f"头像文件: {item['avatar_path']}",file=sys.stderr)
            result = []
        for line in item["info_lines"]:
            #print("  ", line)
            if not line.strip():
                continue
            if line == '\ue683':
                continue
            if line == '\ue682':
                continue
            result.append(line)

        result.append(f"性别:{item['gender']}")
        result.append(f"buttonX:{item['btn_center_x']}")
        result.append(f"buttonY:{item['btn_center_y']}")
        results.append(result)

    # print(f"最终结果:{results}",file=sys.stderr)
    if skipped_empty:
        print(f"\n（已跳过 {skipped_empty} 条无内容且无头像的行）",file=sys.stderr)

    print(f"解析结果: {len(results)} 条, \n耗时 {time.time() - start:.2f} 秒",file=sys.stderr)
    return results




import ctypes
from ctypes import wintypes
import re
from pywinauto import Desktop

# --- Win32 API 准备 ---
user32 = ctypes.windll.user32
GetWindowTextW = user32.GetWindowTextW
GetWindowTextLengthW = user32.GetWindowTextLengthW
IsWindowVisible = user32.IsWindowVisible
EnumWindows = user32.EnumWindows
GetWindowThreadProcessId = user32.GetWindowThreadProcessId

EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)

def _enum_visible_windows():
    """快速枚举所有可见顶层窗口 (hwnd, title, pid)"""
    results = []

    def callback(hwnd, lParam):
        # 仅取可见窗口，避免一些后台/僵尸窗口
        if not IsWindowVisible(hwnd):
            return True
        length = GetWindowTextLengthW(hwnd)
        if length == 0:
            return True
        buf = ctypes.create_unicode_buffer(length + 1)
        GetWindowTextW(hwnd, buf, length + 1)
        # 取 PID
        pid = wintypes.DWORD()
        GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        title = buf.value.strip()
        if title:
            results.append((hwnd, title, int(pid.value)))
        return True

    EnumWindows(EnumWindowsProc(callback), 0)
    return results

def get_pid_by_title(title_keyword, exact=False, visible_only=True, use_uia_fallback=False):
    """
    更快/更稳：先走 Win32，必要时回退 UIA
    :param title_keyword: str，窗口标题关键词或完整标题
    :param exact: bool，是否精确匹配
    :param visible_only: bool，仅匹配可见窗口（默认 True）
    :param use_uia_fallback: bool，Win32 未命中时是否回退到 UIA（默认 False）
    :return: list[int] 匹配到的 PID 列表（去重）
    """
    result_pids = set()

    # ---- 快速路径：Win32 ----
    try:
        windows = _enum_visible_windows() if visible_only else _enum_visible_windows()
        if exact:
            for _, title, pid in windows:
                if title == title_keyword:
                    result_pids.add(pid)
        else:
            key = title_keyword.lower()
            for _, title, pid in windows:
                if key in title.lower():
                    result_pids.add(pid)
    except Exception:
        # Win32 极少失败，失败就走 UIA
        pass

    # 如果已经找到了，直接返回
    if result_pids:
        return list(result_pids)

    # ---- 可选回退：UIA（可能慢，但更“全面”）----
    if use_uia_fallback:
        try:
            desk = Desktop(backend="uia")
            # 用正则/条件在枚举时过滤，避免对每个窗口取属性
            if exact:
                pattern = re.compile(rf"^{re.escape(title_keyword)}$", re.I)
            else:
                pattern = re.compile(re.escape(title_keyword), re.I)

            # 只取顶层、可见、启用的 Window，减少阻塞概率
            wins = desk.windows(
                title_re=pattern,
                visible_only=True if visible_only else False,
                enabled_only=True,
                top_level_only=True
            )
            for w in wins:
                try:
                    result_pids.add(w.process_id())
                except Exception:
                    continue
        except Exception:
            pass

    return list(result_pids)


_cached_boss = None


def get_boss_window(timeout: float = 3.0, refresh: bool = False):
    """
    获取并缓存 BOSS直聘 根控件
    :param timeout: 查找超时
    :param refresh: True 强制刷新（忽略缓存）
    :return: Control 对象 或 None
    """
    global _cached_boss

    if _cached_boss and not refresh:
        try:
            if _cached_boss.Exists(0.5, 0.1):  # 检查是否还有效
                # print('使用缓存',file=sys.stderr)
                return _cached_boss
        except Exception:
            pass

    with auto.UIAutomationInitializerInThread():
        auto.SetGlobalSearchTimeout(0.5)

        # 先 WindowControl
        win = auto.WindowControl(RegexName=r'.*BOSS直聘.*', searchDepth=1)
        if win.Exists(timeout, 0.2):
            _cached_boss = win
            return win

        # 回退 PaneControl
        root = auto.GetRootControl()
        for c in root.GetChildren():
            if 'BOSS直聘' in (c.Name or '') and c.ClassName in ('Chrome_WidgetWin_0', 'Chrome_WidgetWin_1'):
                _cached_boss = c
                return c

    return None

import psutil

def kill_wps_cloud():
    """关闭 WPS 云服务进程"""
    pids = []
    for proc in psutil.process_iter(["name", "pid"]):
        if proc.info["name"].lower() == "wpscloudsvr.exe":
            pids.append(proc.info["pid"])
    if not pids:
        return True  # 未找到进程，无需关闭
    try:
        for pid in pids:
            psutil.Process(pid).terminate()  # 优雅关闭
        time.sleep(1)  # 等待进程退出
        return True
    except Exception as e:
        print(f"关闭 WPS 云服务失败: {e}")
        return False


import time
import os
import ctypes
import ctypes.wintypes as wintypes
import psutil

last_step_time = time.time()

def mark_step():
    global last_step_time
    last_step_time = time.time()


# ----------- WCT 识别阻塞进程（同步版）-----------
WCT_AVAILABLE = False
try:
    advapi32 = ctypes.windll.advapi32
    OpenThreadWaitChainSession = advapi32.OpenThreadWaitChainSession
    GetThreadWaitChain = advapi32.GetThreadWaitChain
    CloseThreadWaitChainSession = advapi32.CloseThreadWaitChainSession
    WCT_AVAILABLE = True
except Exception:
    WCT_AVAILABLE = False


class WAITCHAIN_NODE_INFO(ctypes.Structure):
    _fields_ = [
        ("ObjectType", wintypes.DWORD),
        ("ObjectStatus", wintypes.DWORD),
        ("ThreadId", wintypes.DWORD),
        ("ProcessId", wintypes.DWORD),
        ("WaitTime", wintypes.DWORD),
        ("ContextSwitches", wintypes.DWORD)
    ]


def detect_blockers_wct():
    if not WCT_AVAILABLE:
        return []

    try:
        session = OpenThreadWaitChainSession(0, 0)
        if not session:
            return []

        arr = (WAITCHAIN_NODE_INFO * 64)()
        count = wintypes.DWORD(64)
        dead = wintypes.BOOL()

        kernel32 = ctypes.windll.kernel32
        tid = kernel32.GetCurrentThreadId()

        res = GetThreadWaitChain(session, 0, tid, ctypes.byref(count), arr, ctypes.byref(dead))
        CloseThreadWaitChainSession(session)

        if res == 0:
            return []

        pids = []
        for i in range(count.value):
            pid = arr[i].ProcessId
            if pid != 0 and pid != os.getpid():
                pids.append(pid)

        return list(set(pids))
    except:
        return []


def kill_blockers(pids):
    for pid in pids:
        try:
            p = psutil.Process(pid)
            print(f"[同步修复] 杀掉阻塞进程 {p.name()} ({pid})", flush=True)
            p.terminate()
            time.sleep(0.2)
            if p.is_running():
                p.kill()
        except:
            pass


# ----------- 同步检查卡住点 -----------
def check_blocking(timeout_ms=2000):
    global last_step_time

    now = time.time()
    if (now - last_step_time) * 1000 < timeout_ms:
        return  # 没卡

    print(f"[警告] 检测到执行停顿超过 {timeout_ms} ms，正在分析阻塞...", flush=True)

    pids = detect_blockers_wct()
    if pids:
        print("[WCT] 找到阻塞进程：", pids, flush=True)
        kill_blockers(pids)
        print("[已修复] 阻塞进程已终止", flush=True)
    else:
        print("[WCT] 未发现阻塞进程", flush=True)

    # 重置计时
    last_step_time = time.time()




import threading
import time

class SyncTimeoutMonitor:
    def __init__(self):
        self.running = False
        self.start_time = None
        self.timeout = None
        self.killed = False
        self.thread = None

    def start(self, timeout):
        self.running = True
        self.start_time = time.time()
        self.timeout = timeout
        self.killed = False

        def monitor():
            while self.running:
                elapsed = time.time() - self.start_time
                if elapsed > self.timeout:
                    print(f"[监控] 方法执行超过 {self.timeout}s，检测阻塞进程...", flush=True)
                    pids = detect_blockers_wct()
                    kill_blockers(pids)
                    self.killed = True
                    return
                time.sleep(0.1)

        self.thread = threading.Thread(target=monitor, daemon=True)
        self.thread.start()

    def stop(self):
        self.running = False


def sync_safe_call(func, timeout):
    monitor = SyncTimeoutMonitor()
    monitor.start(timeout)

    try:
        result = func()   # 🔥 🔥 🔥 同步执行，不开线程
        return result
    finally:
        monitor.stop()



import test






# 主程序
if __name__ == "__main__":

    kill_wps_cloud()

    print(json.dumps({"results": {"firstRunStatus":True}}, ensure_ascii=False), flush=True)

    if len(sys.argv) < 2:
        # ====== 新增：循环模式，持续读取 stdin ======
        print("READY", flush=True)
        while True:
            try:
                line = sys.stdin.readline()
                # print('python端接收的数据：', line, file=sys.stderr)
                if not line:
                    # print('----break-----', line, file=sys.stderr)
                    break
                line = line.strip()
                if not line:
                    # print('----continue-----', line, file=sys.stderr)
                    continue
                ctx = json.loads(line)
                template_path = ctx.get('template_path')
                region = ctx.get('region')
                typeContext = ctx.get('type', 'image')
                confidence = ctx.get('confidence', 0.9)
                # 初始化 OCR（仅在 type==word 时）
                # if typeContext == "word" and TARGET_PID is None:
                #     print('初始化 进程ID（仅在 type==word 时）', file=sys.stderr)
                #     pids = get_pid_by_title('BOSS直聘')
                #     print('BOSS直聘进程ID:', pids, file=sys.stderr)
                #
                #     if pids is None or len(pids) == 0:
                #         print('没有获取到BOSS直聘进程ID,请确认是否启动BOSS直聘', file=sys.stderr)
                #     else:
                #         TARGET_PID = pids[0]

                if typeContext == 'image':
                    # print('image匹配-------', file=sys.stderr)
                    results = get_matches_with_confidence(template_path, confidence=confidence, region=region)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)
                elif typeContext == 'word':

                    # print('用户信息', file=sys.stderr)
                    results = get_user_info()
                    # print("识别用户信息结果：",results,file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'ImageSingleCoordinate':
                    results = get_matches_one_with_confidence(template_path, threshold=confidence,region=region)
                    print(f'ImageSingleCoordinate识别结果:{results}', file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'click':
                    # print('点击',file=sys.stderr)
                    main_win = get_win_filter_candidate_window()
                    results = filter_candidate.click(main_win = main_win,title=ctx.get('title'),control_type=ctx.get('control_type'),found_index=ctx.get('found_index'))
                    print(f'点击结果：{results}',file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'message':
                    # print('获取消息列表',file=sys.stderr)
                    start = time.time()
                    results =  get_message.get_message_list(get_boss_window())
                    duration = time.time() - start
                    print(f'消息列表耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('获取消息结果',results,file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'messageDetail':
                    # print('获取聊天记录',file=sys.stderr)
                    start = time.time()

                    refresh =  ctx.get('refresh')
                    if refresh:
                        get_boss_window(True)

                    results = get_message_detail.get_message_list(get_boss_window())
                    duration = time.time() - start
                    print(f'聊天记录耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('聊天记录结果',results,file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'inputMessage':
                    print('输入消息',file=sys.stderr)
                    start = time.time()
                    results = input_message.input_message(get_boss_window(),ctx.get('message'))
                    duration = time.time() - start
                    print(f'输入消息耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('输入消息结果',results,file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'refreshPywinautoWindow':
                    print('刷新refreshPywinautoWindow',file=sys.stdout)
                    start = time.time()
                    get_win_filter_candidate_window(True)
                    duration = time.time() - start
                    print(f'执行刷新refreshPywinautoWindow：{duration}秒', file=sys.stderr)
                    print(json.dumps({"results": True}, ensure_ascii=False), flush=True)

                elif typeContext == 'refreshUIautoWindow':
                    print('刷新refreshUIautoWindow',file=sys.stdout)
                    start = time.time()
                    get_boss_window(True)
                    duration = time.time() - start
                    print(f'刷新refreshUIautoWindow：{duration:.2f}秒', file=sys.stderr)
                    print(json.dumps({"results": True}, ensure_ascii=False), flush=True)

                elif typeContext == 'choosePost':
                    print('开始选择职位', file=sys.stderr)
                    start = time.time()
                    results = choose_post.execute(get_boss_window(), ctx.get('post'))
                    duration = time.time() - start
                    print(f'选择职位耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('选择职位结果', results, file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)


                elif typeContext == 'changeWindow':
                    print('开始窗口操作',file=sys.stderr)
                    results = fit_window_left_top_uia(ctx.get('windowName'), ctx.get('windowSize'))
                    print('窗口操作结果',results,file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'syncPost':
                    print('开始同步职位', file=sys.stderr)
                    start = time.time()
                    results = sync_post.execute(get_boss_window())
                    duration = time.time() - start
                    print(f'同步职位耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('同步职位结果', results, file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'messagePersonInfo':
                    # print('获取个人信息', file=sys.stderr)
                    start = time.time()
                    results = get_message_person_info.execute(get_boss_window())
                    duration = time.time() - start
                    print(f'获取个人信息耗时：{duration:.2f}秒', file=sys.stderr)
                    # print('获取消息详情个人信息结果', results, file=sys.stderr)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'click_cancel_filter':
                    print(f'点击取消筛选', file=sys.stdout)
                    results = click_cancel_filter.execute(get_boss_window())
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'get_message_choose_post':
                    print(f'点击消息职位', file=sys.stdout)
                    results = get_message_choose_post.execute(get_boss_window(), ctx.get('post'))
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                elif typeContext == 'quit':
                    print('收到退出指令，准备退出进程', file=sys.stdout)
                    print(json.dumps({"results": True, "op": "quit"}, ensure_ascii=False), flush=True)
                    sys.exit(0)  # 或者 os._exit(0)，更强制

                elif typeContext == 'test':
                    results = sync_safe_call(lambda: test.hello(), timeout=4)
                    print(json.dumps({"results": results}, ensure_ascii=False), flush=True)

                else:
                    print(json.dumps({"error": "类型错误"}), flush=True)
            except Exception as e:
                print(json.dumps({"error": str(e)}), flush=True)
